/*
 * Copyright 1999-2101 Alibaba Group.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package com.alibaba.fastjson.parser;

import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.math.BigDecimal;
import java.math.BigInteger;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;

import com.alibaba.fastjson.annotation.JSONField;

/**
 * 
 * @author wenshao<szujobs@hotmail.com>
 * 
 */
@SuppressWarnings("rawtypes")
public class JavaBeanMapping {
	private static JavaBeanMapping global = new JavaBeanMapping();

	public static JavaBeanMapping getGlobalInstance() {
		return global;
	}
	
	private final Set<Class<?>> primitiveClasses = new HashSet<Class<?>>();

	private final ConcurrentMap<Class<?>, Map<String, Method>> settersMap = new ConcurrentHashMap<Class<?>, Map<String, Method>>();
	private final ConcurrentMap<Class<?>, Map<String, Method>> gettersMap = new ConcurrentHashMap<Class<?>, Map<String, Method>>();
	private final ConcurrentMap<Class<?>, Map<Integer, Enum>> enumValues = new ConcurrentHashMap<Class<?>, Map<Integer, Enum>>();

	public JavaBeanMapping() {
		primitiveClasses.add(boolean.class);
		primitiveClasses.add(Boolean.class);
		
		primitiveClasses.add(char.class);
		primitiveClasses.add(Character.class);
		
		primitiveClasses.add(byte.class);
		primitiveClasses.add(Byte.class);
		
		primitiveClasses.add(short.class);
		primitiveClasses.add(Short.class);

		primitiveClasses.add(int.class);
		primitiveClasses.add(Integer.class);

		primitiveClasses.add(long.class);
		primitiveClasses.add(Long.class);
		
		primitiveClasses.add(float.class);
		primitiveClasses.add(Float.class);
		
		primitiveClasses.add(double.class);
		primitiveClasses.add(Double.class);
		
		primitiveClasses.add(BigInteger.class);
		primitiveClasses.add(BigDecimal.class);
		
		primitiveClasses.add(String.class);
		primitiveClasses.add(java.util.Date.class);
		primitiveClasses.add(java.sql.Date.class);
		primitiveClasses.add(java.sql.Time.class);
		primitiveClasses.add(java.sql.Timestamp.class);
	}
	
	public boolean isPrimitive(Class<?> clazz) {
		return primitiveClasses.contains(clazz);
	}

	public void clear() {
		settersMap.clear();
		enumValues.clear();
	}

	public ConcurrentMap<Class<?>, Map<Integer, Enum>> getEnumValues() {
		return enumValues;
	}
	
	public Map<String, Method> getGetters(Class<?> clazz) {
		return this.getGetters(clazz, null);
	}
	
	public Map<String, Method> getGetters(Class<?> clazz, Map<String, String> aliasMap) {
		Map<String, Method> getters = gettersMap.get(clazz);

		if (getters != null) {
			return getters;
		}
		
		getters = new HashMap<String, Method>();
		for (Method method : clazz.getMethods()) {
			String methodName = method.getName();

			if (Modifier.isStatic(method.getModifiers())) {
				continue;
			}

			if (method.getReturnType().equals(Void.TYPE)) {
				continue;
			}

			if (method.getParameterTypes().length != 0) {
				continue;
			}

			JSONField annotation = method.getAnnotation(JSONField.class);

			if (annotation != null) {
				if (!annotation.serialize()) {
					continue;
				}

				if (annotation.name().length() != 0) {
					String propertyName = annotation.name();
					
					if (aliasMap != null) {
						propertyName = aliasMap.get(propertyName);
						if (propertyName == null) {
							continue;
						}
					}
					
					getters.put(propertyName, method);
					continue;
				}
			}

			if (methodName.startsWith("get")) {
				if (methodName.length() < 4) {
					continue;
				}

				if (methodName.equals("getClass")) {
					continue;
				}

				if (!Character.isUpperCase(methodName.charAt(3))) {
					continue;
				}
				
				String propertyName = Character.toLowerCase(methodName.charAt(3)) + methodName.substring(4);
				
				Field field = getField(clazz, propertyName);
				if (field != null) {
					if (Modifier.isTransient(field.getModifiers())) {
						continue;
					}
					
					JSONField fieldAnnotation = field.getAnnotation(JSONField.class);
					
					if (fieldAnnotation != null && fieldAnnotation.name().length() != 0) {
						propertyName = fieldAnnotation.name();
						
						if (aliasMap != null) {
							propertyName = aliasMap.get(propertyName);
							if (propertyName == null) {
								continue;
							}
						}
						
						getters.put(propertyName, method);
						continue;
					}
				}
				
				if (aliasMap != null) {
					propertyName = aliasMap.get(propertyName);
					if (propertyName == null) {
						continue;
					}
				}
				
				getters.put(propertyName, method);
			}

			if (methodName.startsWith("is")) {
				if (methodName.length() < 3) {
					continue;
				}

				if (!Character.isUpperCase(methodName.charAt(2))) {
					continue;
				}
				
				String propertyName = Character.toLowerCase(methodName.charAt(2)) + methodName.substring(3);
				
				Field field = getField(clazz, propertyName);
				if (field != null) {
					JSONField fieldAnnotation = field.getAnnotation(JSONField.class);
					
					if (fieldAnnotation != null && fieldAnnotation.name().length() != 0) {
						propertyName = fieldAnnotation.name();
						
						if (aliasMap != null) {
							propertyName = aliasMap.get(propertyName);
							if (propertyName == null) {
								continue;
							}
						}
						
						getters.put(propertyName, method);
						continue;
					}
				}
				
				if (aliasMap != null) {
					propertyName = aliasMap.get(propertyName);
					if (propertyName == null) {
						continue;
					}
				}

				getters.put(propertyName, method);
			}
		}
		
		gettersMap.putIfAbsent(clazz, getters);
		
		return gettersMap.get(clazz);
	}
	
	public static Field getField(Class<?> clazz, String fieldName) {
		try {
			return clazz.getDeclaredField(fieldName);
		} catch (Exception e) {
			return null;
		}
	}

	public Map<String, Method> getSetters(Class<?> clazz) {
		return getSetters(clazz, null);
	}
	
	public Map<String, Method> getSetters(Class<?> clazz, Map<String, String> aliasMap) {
		Map<String, Method> setters = settersMap.get(clazz);

		if (setters != null) {
			return setters;
		}

		setters = new HashMap<String, Method>();
		for (Method method : clazz.getMethods()) {
			String methodName = method.getName();
			if (methodName.length() < 4) {
				continue;
			}

			if (Modifier.isStatic(method.getModifiers())) {
				continue;
			}

			if (!method.getReturnType().equals(Void.TYPE)) {
				continue;
			}

			if (method.getParameterTypes().length != 1) {
				continue;
			}

			JSONField annotation = method.getAnnotation(JSONField.class);

			if (annotation != null) {
				if (!annotation.deserialize()) {
					continue;
				}

				if (annotation.name().length() != 0) {
					String propertyName = annotation.name();
					setters.put(propertyName, method);
					method.setAccessible(true);
					continue;
				}
			}

			if (methodName.startsWith("set") && Character.isUpperCase(methodName.charAt(3))) {
				String propertyName = Character.toLowerCase(methodName.charAt(3)) + methodName.substring(4);
				
				Field field = getField(clazz, propertyName);
				if (field != null) {
					if (Modifier.isTransient(field.getModifiers())) {
						continue;
					}
					
					JSONField fieldAnnotation = field.getAnnotation(JSONField.class);
					
					if (fieldAnnotation != null && fieldAnnotation.name().length() != 0) {
						propertyName = fieldAnnotation.name();
						
						if (aliasMap != null) {
							propertyName = aliasMap.get(propertyName);
							if (propertyName == null) {
								continue;
							}
						}
						
						setters.put(propertyName, method);
						continue;
					}
				}
				
				setters.put(propertyName, method);
				method.setAccessible(true);
			}
		}
		settersMap.putIfAbsent(clazz, setters);
		
		return settersMap.get(clazz);
	}
}
